/*
 * Copyright (c) 2003-2005
 * XDoclet Team
 * All rights reserved.
 */
package org.xdoclet.plugin.hibernate;

import java.io.File;

import java.util.*;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.generama.JellyTemplateEngine;
import org.generama.QDoxCapableMetadataProvider;
import org.generama.WriterMapper;

import org.generama.defaults.XMLOutputValidator;

import org.xdoclet.plugin.hibernate.qtags.HibernateFilterParamTag;
import org.xdoclet.plugin.hibernate.qtags.HibernateTypedefParamTag;
import org.xdoclet.plugin.hibernate.qtags.TagLibrary;

import com.thoughtworks.qdox.model.AbstractJavaEntity;
import com.thoughtworks.qdox.model.BeanProperty;
import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.JavaMethod;

/**
 * Plugin producing hibernate mapping declarations
 *
 * @author Konstantin Pribluda
 * @author Anatol Pomozov
 */
public class HibernateMappingPlugin extends AbstractHibernatePlugin {
    static final String TAG_PREFIX = "hibernate.";
    static final Log log = LogFactory.getLog(HibernateMappingPlugin.class);
    static final Map tagDispatch = new HashMap();

    // list holding tag names defining property existence
    static final List PROPERTY_TAGS = new ArrayList();

    // list holding tag names defining property existence
    static final List ALLOWED_IN_PROPERTIES_TAGS = new ArrayList();
    
    // list holding tag names allowed in join tag
    static final List ALLOWED_IN_JOIN_TAGS = new ArrayList();

    // list holding tag names causing stop of recursive search for properties
    static final List HIERARCHY_STOP_TAGS = new ArrayList();

    // list holding tag names defining ID existence
    static final List ID_TAGS = new ArrayList();

    // list holding tag names defining ID existence
    static final List VERSION_TAGS = new ArrayList();

    static {
        PROPERTY_TAGS.add("hibernate.property");
        PROPERTY_TAGS.add("hibernate.many-to-one");
        PROPERTY_TAGS.add("hibernate.one-to-one");
        PROPERTY_TAGS.add("hibernate.component");
        PROPERTY_TAGS.add("hibernate.dynamic-component");
        PROPERTY_TAGS.add("hibernate.any");
        PROPERTY_TAGS.add("hibernate.map");
        PROPERTY_TAGS.add("hibernate.set");
        PROPERTY_TAGS.add("hibernate.list");
        PROPERTY_TAGS.add("hibernate.bag");
        PROPERTY_TAGS.add("hibernate.idbag");
        PROPERTY_TAGS.add("hibernate.array");
        PROPERTY_TAGS.add("hibernate.primitive-array");
        PROPERTY_TAGS.add("hibernate.key-property");
        PROPERTY_TAGS.add("hibernate.key-many-to-one");
        PROPERTY_TAGS.add("hibernate.parent");
        HIERARCHY_STOP_TAGS.add("hibernate.class");
        HIERARCHY_STOP_TAGS.add("hibernate.subclass");
        HIERARCHY_STOP_TAGS.add("hibernate.joined-subclass");
        HIERARCHY_STOP_TAGS.add("hibernate.union-subclass");
        ID_TAGS.add("hibernate.id");
        ID_TAGS.add("hibernate.composite-id");
        VERSION_TAGS.add("hibernate.version");
        VERSION_TAGS.add("hibernate.timestamp");
        ALLOWED_IN_PROPERTIES_TAGS.add("hibernate.property");
        ALLOWED_IN_PROPERTIES_TAGS.add("hibernate.many-to-one");
        ALLOWED_IN_PROPERTIES_TAGS.add("hibernate.component");
        ALLOWED_IN_PROPERTIES_TAGS.add("hibernate.dynamic-component");
        ALLOWED_IN_JOIN_TAGS.add("hibernate.property");
        ALLOWED_IN_JOIN_TAGS.add("hibernate.many-to-one");
        ALLOWED_IN_JOIN_TAGS.add("hibernate.component");
        ALLOWED_IN_JOIN_TAGS.add("hibernate.dynamic-component");
    }

    private boolean force = false;
    private Stack componentsPrefixies = new Stack();

    public HibernateMappingPlugin(JellyTemplateEngine jellyTemplateEngine,
        QDoxCapableMetadataProvider metadataProvider, WriterMapper writerMapper) {
        super(jellyTemplateEngine, metadataProvider, writerMapper);
        setFileregex("\\.java");
        setFilereplace("\\.hbm.xml");
        setMultioutput(true);
        Map dtds = new HashMap();
        dtds.put("http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd",
            getClass().getResource("dtd/hibernate-mapping-2.0.dtd"));
        dtds.put("http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd",
            getClass().getResource("dtd/hibernate-mapping-3.0.dtd"));
        setOutputValidator(new XMLOutputValidator(dtds));
        new TagLibrary(metadataProvider);
    }

    /**
     * Get all candidates for properties tag
     */
    public List getAlternateKeyProperties(JavaClass clazz, final String propertiesName) {
        if (propertiesName == null) {
            return null;
        }

        List retval = new ArrayList();
        accumulatePropertiesRecursive(clazz, null, true, ALLOWED_IN_PROPERTIES_TAGS, retval);
        CollectionUtils.filter(retval,
            new Predicate() {
                public boolean evaluate(Object object) {
                    HibernateProperty prop = (HibernateProperty) object;
                    DocletTag tag = getTagByNameList(prop.getEntity(), ALLOWED_IN_PROPERTIES_TAGS);

                    return tag != null && propertiesName.equalsIgnoreCase(tag.getNamedParameter("properties-name"));

                    }
            });
        return retval;
    }

    /**
     * Get all candidates for join tag
     */
    public List getJoinProperties(JavaClass clazz, final String joinName) {
        if (joinName == null) {
            return null;
        }

        List retval = new ArrayList();
        accumulatePropertiesRecursive(clazz, null, true, ALLOWED_IN_JOIN_TAGS, retval);
        CollectionUtils.filter(retval,
            new Predicate() {
                public boolean evaluate(Object object) {
                    HibernateProperty prop = (HibernateProperty) object;
                    DocletTag tag = getTagByNameList(prop.getEntity(), ALLOWED_IN_JOIN_TAGS);

                    return tag != null && joinName.equalsIgnoreCase(tag.getNamedParameter("join-name"));

                    }
            });
        return retval;
    }

    /**
     * Get at least one doclet tag from javaEntity
     * @param entity entity which have doclet tags
     * @param tagNames list of possible tag names
     * @return found doclet tag or null if there is no such tag
     */
    public DocletTag getTagByNameList(AbstractJavaEntity entity, List tagNames) {
        for (int i = 0; i < tagNames.size(); i++) {
            String tag = (String) tagNames.get(i);
            DocletTag docletTag = entity.getTagByName(tag);

            if (docletTag != null) {
                return docletTag;
            }
        }

        return null;
    }

    /**
     * provide list of properties candidating for class id
     *
     * @param clazz
     * @return
     */
    public List getClassId(JavaClass clazz) {
        List retval = new ArrayList();
        accumulatePropertiesRecursive(clazz, null, true, ID_TAGS, retval);
        return retval;
    }

    /**
     * provide list of hibernate properties for class. it could be getter, as
     * well as field ( for direct property access )
     */
    public List getClassProperties(JavaClass clazz) {
        List retval = new ArrayList();
        accumulatePropertiesRecursive(clazz, null, true, PROPERTY_TAGS, retval);
        return retval;
    }

    /**
     * Get list of properties for component class. Real class is calculated in following sequence.
     * First tries to find class by its name if classname parameter is null or class not found
     * @param componentClassName
     * @param componentPropertyClass
     * @return
     */
    public List getComponentProperties(String componentClassName, JavaClass componentPropertyClass) {
        JavaClass componentClass;

        if (componentClassName != null) {
            componentClass = getMetadata(componentClassName);
        } else {
            componentClass = componentPropertyClass;
        }

        return getClassProperties(componentClass);
    }

    /**
     * Returns first argument wich is not empty or last argument if all are empty
     *
     * @param value1 first argument
     * @param value2 second argument
     * @return
     */
    public String getFirstNonEmptyValue(String value1, String value2) {
        return value1 != null && value1.trim().length() > 0 ? value1 : value2;
    }

    public void setForce(boolean force) {
        this.force = force;
    }

    /**
     * Return the instance of JavaClass by it's name
     */
    public JavaClass getMetadata(final String className) {
        if (className == null) {
            throw new NullPointerException("Classname can't be null");
        }

        JavaClass javaClass = (JavaClass) CollectionUtils.find(getMetadata(),
                new Predicate() {
                    public boolean evaluate(Object o) {
                        JavaClass cl = (JavaClass) o;
                        return cl.getFullyQualifiedName().equals(className);
                    }
                });

        if (javaClass == null) {
            log.error("Sourcecode for class '" + className + "' not found by metadata povider");
            throw new IllegalArgumentException("Class metadata for " + className + " not found");
        }

        return javaClass;
    }

    /**
     * derive property name from metadata. strip / decapitalize
     */
    public String getPropertyName(AbstractJavaEntity metadata) {
        if (metadata instanceof JavaMethod) {
            return ((JavaMethod) metadata).getPropertyName();
        } else if (metadata instanceof JavaField) {
            return metadata.getName();
        }

        // maybe throw exception?
        return null;
    }

    /**
     * provide list of valid property tags
     */
    public List getPropertyTagList() {
        return PROPERTY_TAGS;
    }

    /**
     * provide list of valid property tags that can be used inside <properties> tag
     */
    public List getTagListAllowedInProperties() {
        return ALLOWED_IN_PROPERTIES_TAGS;
    }

    /**
     * provide list of valid property tags that can be used inside <join> tag
     */
    public List getTagListAllowedInJoin() {
        return ALLOWED_IN_JOIN_TAGS;
    }

    /**
     * provide list of hibernate properties for subclass. it could be getter, as
     * well as field ( for direct property access ). we stop at hibernate.class
     */
    public List getSubclassProperties(JavaClass clazz) {
        List retval = new ArrayList();
        accumulatePropertiesRecursive(clazz, HIERARCHY_STOP_TAGS, true, PROPERTY_TAGS, retval);
        return retval;
    }

    /**
     * provide list of subclasses for given class
     */
    public List getSubclasses(JavaClass clazz, String tagName) {
        List result = new ArrayList();
        getSubclassesWithTagRecursive(result, clazz, tagName);
        Collections.sort(result);
        return result;
    }

    /**
     * provide combined list of all the tags with given tag names
     */
    public List getTags(AbstractJavaEntity metadata, Collection tagNames) {
        List al = new ArrayList();

        for (Iterator iter = tagNames.iterator(); iter.hasNext();) {
            al.addAll(Arrays.asList(metadata.getTagsByName((String) iter.next())));
        }

        return al;
    }

    /**
     * provide list of tags from pipe separated string
     */
    public List getTags(AbstractJavaEntity metadata, String tagNames) {
        return getTags(metadata, Arrays.asList(tagNames.split("\\|")));
    }

    public Collection getTypedefParams(JavaClass clazz, final String typedefName) {
        if (typedefName == null) {
            return null;
        }

        Collection typedefTags = new ArrayList();
        typedefTags.addAll(Arrays.asList(clazz.getTagsByName("hibernate.typedef-param")));
        CollectionUtils.filter(typedefTags,
            new Predicate() {
                public boolean evaluate(Object object) {
                    HibernateTypedefParamTag tag = (HibernateTypedefParamTag) object;
                    return typedefName.equals(tag.getTypedefName());
                }
            });
        return typedefTags;
    }

    public Collection getFilterdefParams(JavaClass clazz, final String filterdefName) {
        if (filterdefName == null) {
            return null;
        }
        Collection filterdefTags = new ArrayList();
        filterdefTags.addAll(Arrays.asList(clazz.getTagsByName("hibernate.filter-param", true)));
        CollectionUtils.filter(filterdefTags,
            new Predicate() {
                public boolean evaluate(Object object) {
                    HibernateFilterParamTag tag = (HibernateFilterParamTag) object;
                    return filterdefName.equals(tag.getFilterdefName());
                }
            });
        return filterdefTags;
    }

    /**
     * provide list of properties candidating for version or timestamp
     */
    public List getVersionOrTimestamp(JavaClass clazz) {
        List retval = new ArrayList();
        accumulatePropertiesRecursive(clazz, null, true, VERSION_TAGS, retval);
        return retval;
    }

    /**
     * dispatch qtag to correct jelly script. cache results statically
     */
    public String dispatchTag(String tagName) {
        String retval = (String) tagDispatch.get(tagName);

        if (retval == null) {
            // rig up correct script name from tag name
            // first, kill hibernate
            if (!tagName.startsWith(TAG_PREFIX)) {
                return null;
            }

            // split on dash and capitalize first chars
            String[] nameParts = tagName.substring(TAG_PREFIX.length()).split("-");
            StringBuffer scriptName = new StringBuffer("/");

            for (int i = 0; i < nameParts.length; i++) {
                scriptName.append(Character.toUpperCase(nameParts[i].charAt(0)));
                scriptName.append(nameParts[i].substring(1));
            }

            scriptName.append(".jelly");
            retval = scriptName.toString();
            log.debug("Dispatch tag " + tagName + " to script " + retval);
            tagDispatch.put(tagName, retval);
        }

        return retval;
    }

    public boolean hasAtLeastOne(AbstractJavaEntity metadata, Collection tags) {
        return getTags(metadata, tags).size() >= 1;
    }

    public boolean hasAtMostOne(AbstractJavaEntity metadata, Collection tags) {
        return getTags(metadata, tags).size() <= 1;
    }

    public boolean hasOnlyOne(AbstractJavaEntity metadata, Collection tags) {
        return getTags(metadata, tags).size() == 1;
    }

    /**
     * whether we sould generate given class. we generate if class contains
     * hibernate.class tag on it. class could be as well abstract, because real
     * stuff lives in polymorphic subclasses
     */
    public boolean shouldGenerate(Object metadata) {
        JavaClass clazz = (JavaClass) metadata;

        //Check if mapping up-to-date then skip generation
        if (!force) {
            String packagePath = getDestinationPackage(metadata).replace('.', '/');
            File dir = new File(getDestdirFile(), packagePath);
            String filename = getDestinationFilename(metadata);
            File destFile = new File(dir, filename);
            File sourceFile = new File(clazz.getSource().getURL().getFile());

            if (destFile.exists() && sourceFile.lastModified() < destFile.lastModified()) {
                return false;
            }
        }

        // we refuse to generate if object is in restricted folder
        if(!super.shouldGenerate(metadata)) {
        	return false;
        }
        boolean generate = clazz.getTagByName("hibernate.class") != null;

        if (generate) {
            System.out.println("  * Generate mapping for '" + clazz.getName() + "' entity");
        }

        return generate;
    }

    /**
     * gather hibernate propertis from given class into list
     */
    private void accumulateProperties(JavaClass clazz, Collection requieredTags, List accumulate) {
        // walk through property getters
        BeanProperty[] beanProperties = clazz.getBeanProperties();
        HibernateProperty property;

        for (int i = 0; i < beanProperties.length; i++) {
            // property is ours, if we have at least one of designated property
            // tags and there is accessor
        	
            if ((beanProperties[i] != null) && (beanProperties[i].getAccessor() != null) &&
                    !getTags(beanProperties[i].getAccessor(), requieredTags).isEmpty()) {
                property = new HibernateProperty();
                property.setName(beanProperties[i].getName());
                property.setEntity(beanProperties[i].getAccessor());
                // we do not specify access setting unless there is explicit
                // setting
                if(((DocletTag)getTags(beanProperties[i].getAccessor(),requieredTags).get(0)).getNamedParameter("access")!= null) {
                	property.setAccess(((DocletTag)getTags(beanProperties[i].getAccessor(),requieredTags).get(0)).getNamedParameter("access"));
                } 
                
                if (!accumulate.contains(property)) {
                    accumulate.add(property);
                }
            }
        }

        JavaField[] fields = clazz.getFields();

        for (int i = 0; i < fields.length; i++) {
            if (!getTags(fields[i], requieredTags).isEmpty()) {
                property = new HibernateProperty();
                property.setName(fields[i].getName());
                property.setEntity(fields[i]);
                // if no property access is specified, we shall use field
                // else take whatever  user says ( leave it to his discretion to 
                // use  wrong specification ) 
                if(((DocletTag)getTags(fields[i], requieredTags).get(0)).getNamedParameter("access") != null) {
                	property.setAccess(((DocletTag)getTags(fields[i], requieredTags).get(0)).getNamedParameter("access"));
                } else {
                	property.setAccess("field");
                }

                if (!accumulate.contains(property)) {
                    accumulate.add(property);
                }
            }
        }
    }

    /**
     * recursive property retrival stopping at stop tag
     * @param skipStopTags needs more explanation here. We need to skip checking on stop tags on the first method invocation
     * (I mean when we didnt dive into recursion cycle yet) because it have already @hibernate.property tag (which is stop tag by itself)
     */
    private void accumulatePropertiesRecursive(JavaClass clazz, Collection stopTags, boolean skipStopTags,
        Collection requiredTags, List accumulate) {
        // stop recursion?
        if (!skipStopTags && stopTags != null && !getTags(clazz, stopTags).isEmpty()) {
            // yep.
            return;
        }

        accumulateProperties(clazz, requiredTags, accumulate);
        //Look at subclass
        JavaClass superclass = clazz.getSuperJavaClass();

        if (superclass != null) {
            accumulatePropertiesRecursive(superclass, stopTags, false, requiredTags, accumulate);
        }

        //Browse over all implemented interfaces
        JavaClass[] ifaces = clazz.getImplementedInterfaces();

        for (int i = 0; i < ifaces.length; i++) {
            accumulatePropertiesRecursive(ifaces[i], stopTags, false, requiredTags, accumulate);
        }
    }

    /**
     * recursively retrieve list of derived classes with certain tag, cut search
     * on found tag. also preveent double addition of subclass to the list ( 
     * due to interface inheritance ) 
     */
    private void getSubclassesWithTagRecursive(List found, JavaClass clazz, String tagName) {
        JavaClass[] subclasses = clazz.getDerivedClasses();

        for (int i = 0; i < subclasses.length; i++) {
            if ((subclasses[i].getSuperJavaClass() == clazz) ||
                    Arrays.asList(subclasses[i].getImplementedInterfaces()).contains(clazz)) {
                if (subclasses[i].getTagByName(tagName) != null) {
                	if(!found.contains(subclasses[i])) {
                		found.add(subclasses[i]);
                	}
                } else {
                    getSubclassesWithTagRecursive(found, subclasses[i], tagName);
                }
            }
        }
    }

    public void startComponent(String prefix) {
        componentsPrefixies.push(prefix);
    }

    public void endComponent() {
        componentsPrefixies.pop();
    }

    public String buildComponentColumnName(String columnName) {
        //If columnName is null then we do not need add prefix
        if (columnName == null) {
            return null;
        }

        if (componentsPrefixies.isEmpty()) {
            return columnName;
        }

        StringBuffer result = new StringBuffer();

        for (Iterator iterator = componentsPrefixies.iterator(); iterator.hasNext();) {
            String pr = (String) iterator.next();

            if (pr != null) {
                result.append(pr);
            }
        }

        result.append(columnName);
        return result.toString();
    }
}
